package com.gee.spring.boot.influxdb.core.query;

import com.gee.spring.boot.influxdb.annotations.Column;
import com.gee.spring.boot.influxdb.core.enums.AndOr;
import com.gee.spring.boot.influxdb.core.enums.AscDesc;
import com.gee.spring.boot.influxdb.core.enums.Condition;
import com.gee.spring.boot.influxdb.core.enums.FillFunc;
import com.gee.spring.boot.influxdb.core.query.domain.ColumnInfo;
import com.gee.spring.boot.influxdb.core.query.domain.MeasurementInfo;
import com.gee.spring.boot.influxdb.core.query.domain.SelectFunc;
import com.gee.spring.boot.influxdb.core.query.domain.lambda.LambdaMeta;
import com.gee.spring.boot.influxdb.core.query.segment.*;
import com.gee.spring.boot.influxdb.core.util.ExceptionUtil;
import com.gee.spring.boot.influxdb.core.util.LambdaUtil;
import com.gee.spring.boot.influxdb.core.util.PropertyUtil;

import java.time.Duration;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import static com.gee.spring.boot.influxdb.core.constant.StrConstant.*;
import static com.gee.spring.boot.influxdb.core.enums.SqlKeyword.*;
import static com.gee.spring.boot.influxdb.core.enums.Condition.*;
import static com.gee.spring.boot.influxdb.core.util.InfluxdbUtil.getMeasurementInfo;

/**
 * @author gepengjun
 * @since 2024/4/28 8:28
 */
public class LambdaQuery<T> {
    //className, fieldName, ColumnInfo

    Class<T> entityClass;

    private final SelectSegmentList selectSegmentList = new SelectSegmentList();

    private final ConditionSegmentList conditionSegmentList = new ConditionSegmentList();

    private final GroupBySegmentList groupBySegmentList = new GroupBySegmentList();

    private final OrderBySegmentList orderBySegmentList = new OrderBySegmentList();

    private final LimitOffsetSegment limitOffsetSegment = new LimitOffsetSegment();

    private final FillSegment fillSegment = new FillSegment();

    private Boolean funcTime = false;

    public Class<T> getEntityClass(){
        return entityClass;
    }


    private LambdaQuery(Class<T> tClass){
        this.entityClass = tClass;
    }

    public static <T> LambdaQuery<T> newQuery(Class<T> tClass){
        return new LambdaQuery<>(tClass);
    }

    public LambdaQuery<T> selectAllWithFuncExceptTime(SelectFunc selectFunc){
        for (ColumnInfo columnInfo : getMeasurementInfo(entityClass).getFieldColumnInfoMap().values()) {
            if (!"time".equals(columnInfo.getColumn().name())){
                addSelect(columnInfo, selectFunc);
            }
        }
        return this;
    }

    public LambdaQuery<T> selectAllWithFunc(SelectFunc selectFunc){
        for (ColumnInfo columnInfo : getMeasurementInfo(entityClass).getFieldColumnInfoMap().values()) {
            addSelect(columnInfo, selectFunc);
        }
        funcTime = true;
        return this;
    }

    @SafeVarargs
    public final LambdaQuery<T> select(SelectFunc selectFunc, SFunction<T, ?>... columns){
        for (SFunction<T, ?> column : columns) {
            select(column, selectFunc);
        }
        return this;
    }



    @SafeVarargs
    public final LambdaQuery<T> select(SFunction<T, ?>... columns){
        for (SFunction<T, ?> column : columns) {
            select(column);
        }
        return this;
    }

    public LambdaQuery<T> select(SFunction<T, ?> column){
        return select(column, null);
    }

    public LambdaQuery<T> select(SFunction<T, ?> column, SelectFunc selectFunc){
        addSelect(getColumnInfo(column), selectFunc);
        return this;
    }

    private void addSelect(ColumnInfo columnInfo, SelectFunc selectFunc){
        Column columnAnnotation = columnInfo.getColumn();
        if (selectFunc != null){
            if (columnAnnotation.tag()){
                throw  ExceptionUtil.re("tag can not with func %s", columnInfo.getField().getName());
            }
            if ("time".equals(columnAnnotation.name())){
                funcTime = true;
            }
        }
        selectSegmentList.add(new SelectSegment(columnInfo, selectFunc));

    }

    public LambdaQuery<T> eq(SFunction<T, ?> column, Object value){
        eq(AndOr.AND, column, value);
        return this;
    }

    public LambdaQuery<T> eq(AndOr andOr, SFunction<T, ?> column, Object value){
        addCondition(andOr, getColumnInfo(column), EQ, value);
        return this;
    }

    public LambdaQuery<T> ne(SFunction<T, ?> column, Object value){
        addCondition(AndOr.AND, getColumnInfo(column), NOT_EQ, value);
        return this;
    }

    public LambdaQuery<T> ne(AndOr andOr, SFunction<T, ?> column, Object value){
        addCondition(andOr, getColumnInfo(column), NOT_EQ, value);
        return this;
    }


    public LambdaQuery<T> lt(SFunction<T, ?> column, Object value){
        return lt(AndOr.AND, column, value);
    }

    public LambdaQuery<T> lt(AndOr andOr, SFunction<T, ?> column, Object value){
        addCondition(andOr, getColumnInfo(column), LT, value);
        return this;
    }

    public LambdaQuery<T> le(SFunction<T, ?> column, Object value){
        return lt(AndOr.AND, column, value);
    }

    public LambdaQuery<T> le(AndOr andOr, SFunction<T, ?> column, Object value){
        addCondition(andOr, getColumnInfo(column), LE, value);
        return this;
    }

    public LambdaQuery<T> gt(SFunction<T, ?> column, Object value){
        return gt(AndOr.AND, column, value);
    }

    public LambdaQuery<T> gt(AndOr andOr, SFunction<T, ?> column, Object value){
        addCondition(andOr, getColumnInfo(column), GT, value);
        return this;
    }

    public LambdaQuery<T> ge(SFunction<T, ?> column, Object value){
        return ge(AndOr.AND, column, value);
    }

    public LambdaQuery<T> ge(AndOr andOr, SFunction<T, ?> column, Object value){
        addCondition(andOr, getColumnInfo(column), GE, value);
        return this;
    }

    private void addCondition(AndOr andOr, ColumnInfo columnInfo, Condition condition, Object value) {
        conditionSegmentList.add(new ConditionSegment(andOr, columnInfo, condition, value));
    }

    public LambdaQuery<T> groupByTime(Duration duration){
        groupBySegmentList.add(new GroupByTimeSegment("time",duration));
        return this;
    }

    public LambdaQuery<T> groupBy(SFunction<T, ?> column){
        groupBySegmentList.add(new GroupBySegment(getColumnInfo(column).getColumn().name()));
        return this;
    }

    public LambdaQuery<T> orderByAsc(SFunction<T, ?> column){
        return orderBy(column, AscDesc.ASC);
    }

    public LambdaQuery<T> orderByDesc(SFunction<T, ?> column){
        return orderBy(column, AscDesc.DESC);
    }

    public LambdaQuery<T> orderBy(String column, AscDesc ascDesc){
        orderBySegmentList.add(new OrderBySegment(getColumnInfo(column), ascDesc));
        return this;
    }

    public LambdaQuery<T> orderBy(SFunction<T, ?> column, AscDesc ascDesc){
        orderBySegmentList.add(new OrderBySegment(getColumnInfo(column), ascDesc));
        return this;
    }

    public LambdaQuery<T> limit(Long limit){
        limitOffsetSegment.setLimit(limit);
        return this;
    }

    public LambdaQuery<T> offset(Long offset){
        limitOffsetSegment.setOffset(offset);
        return this;
    }

    public LambdaQuery<T> and(Supplier<LambdaQuery<T>> supplier){
        ConditionSegmentList childConditionList = supplier.get().conditionSegmentList;
        childConditionList.setAndOr(AndOr.AND);
        conditionSegmentList.add(childConditionList);
        return this;
    }

    public LambdaQuery<T> or(Supplier<LambdaQuery<T>> supplier){
        ConditionSegmentList childConditionList = supplier.get().conditionSegmentList;
        childConditionList.setAndOr(AndOr.OR);
        conditionSegmentList.add(childConditionList);
        return this;
    }

    public LambdaQuery<T> fillByFunc(FillFunc fillFunc){
        fillSegment.setFillValue(fillFunc.getKeyWord());
        fillSegment.setByFunc(true);
        return this;
    }

    public LambdaQuery<T> fillByValue(Object value){
        fillSegment.setFillValue(value);
        return this;
    }


    public String toSqlStr(){
        StringBuilder sqlBuilder = new StringBuilder(SELECT.getKeyword()).append(SPACE);
        MeasurementInfo measurementInfo = getMeasurementInfo(this.entityClass);
        if (selectSegmentList.isEmpty()){
            selectSegmentList.addAll(measurementInfo.getFieldColumnInfoMap().values().stream()
                    .map(columnInfo -> {
                        return new SelectSegment(columnInfo, null);
                    })
                    .collect(Collectors.toList())
            );
        }
        String select = selectSegmentList.stream()
                    .map(SelectSegment::getSqlSegment)
                    .collect(Collectors.joining(COMMA));
        sqlBuilder.append(select).append(SPACE)
                .append(FROM.getKeyword()).append(SPACE).append(measurementInfo.getMeasurement());
        sqlBuilder.append(SPACE).append(getSqlCommon());

        sqlBuilder.append(SPACE).append(limitOffsetSegment.getSqlSegment());
        return sqlBuilder.toString();
    }

    public String toCountSql(){
        StringBuilder sqlBuilder = new StringBuilder(SELECT.getKeyword()).append(SPACE).append("count(time)");
        MeasurementInfo measurementInfo = getMeasurementInfo(this.entityClass);
        sqlBuilder.append(SPACE)
                .append(FROM.getKeyword()).append(SPACE).append(measurementInfo.getMeasurement())
                .append(getSqlCommon());
        return sqlBuilder.toString();
    }


    private String getSqlCommon(){
        StringBuilder sqlBuilder = new StringBuilder();
        if (!conditionSegmentList.isEmpty()){
            sqlBuilder.append(SPACE).append(WHERE.getKeyword()).append(SPACE).append(conditionSegmentList.getSqlSegment());
        }

        if (!groupBySegmentList.isEmpty()){
            sqlBuilder.append(SPACE).append(groupBySegmentList.getSqlSegment());
        }
        if (fillSegment.getFillValue() != null){
            sqlBuilder.append(SPACE).append(fillSegment.getSqlSegment());
        }
        if (!orderBySegmentList.isEmpty()){
            sqlBuilder.append(SPACE).append(orderBySegmentList.getSqlSegment());
        }
        return sqlBuilder.toString();
    }

    private ColumnInfo getColumnInfo(SFunction<T, ?> column) {
        LambdaMeta meta = LambdaUtil.extract(column);
        String fieldName = PropertyUtil.methodToProperty(meta.getImplMethodName());
        Class<?> instantiatedClass = meta.getInstantiatedClass();
        return getMeasurementInfo(instantiatedClass).getColumnInfo(fieldName);
    }

    private ColumnInfo getColumnInfo(String column) {
        return getMeasurementInfo(entityClass).getColumnInfo(column);
    }


    public Boolean getFuncTime() {
        return funcTime;
    }
}
